import { Ref, computed, ref, watch } from "vue";
import { PopoverProps } from "../popover.props";
import { UseHost, UsePosition } from "./types";

interface PopoverPosition {
    popoverLeft: number;
    popoverTop: number;
    arrowLeft: number;
}

export function usePosition(
    props: PopoverProps,
    arrowRef: Ref<any>,
    popoverRef: Ref<any>,
    hostComposition: UseHost
): UsePosition {

    const offsetX = ref(0);
    const originalOffsetX = ref(props.offsetX?.value || 0);
    const popoverWidth = ref(-1);
    const originalReferenceTop = ref(-1);
    const originalReferenceLeft = ref(-1);
    const position = ref(props.placement);
    const arrowLeftPosition = ref(0);
    const popoverLeftPosition = ref(0);
    const popoverTopPosition = ref(0);
    const scrollLeft = ref(document.documentElement.scrollLeft);
    const scrollTop = ref(document.documentElement.scrollTop);

    const { hostLeft, hostTop, hostWidth } = hostComposition;

    watch(props.offsetX, (latestOffsetXValue: number) => {
        offsetX.value = latestOffsetXValue - originalOffsetX.value;
    });

    const popoverStyle = computed(() => {
        const styleObject = {
            left: `${popoverLeftPosition.value}px`,
            top: `${popoverTopPosition.value}px`,
            transform: `translateX(${offsetX.value}px)`
        } as Record<string, any>;
        if (props.zIndex !== -1) {
            styleObject['z-index'] = props.zIndex;
        }
        if (popoverWidth.value !== -1) {
            styleObject.width = `${popoverWidth.value}px`;
        }
        return styleObject;
    });

    const arrowStyle = computed(() => {
        const styleObject = {
            left: `${arrowLeftPosition.value}px`
        } as Record<string, any>;
        return styleObject;
    });

    function fitToReference(referenceElement: HTMLElement) {
        if (referenceElement && props.keepWidthWithReference) {
            const referenceRect = referenceElement.getBoundingClientRect();
            popoverWidth.value = Math.max(props.minWidth, referenceRect.width);
        }
    }

    function calculatePopoverLeftLimit(popoverRect: DOMRect, intendingToPopoverLeft: number) {
        const leftLimit = hostWidth.value - popoverRect.width - 4;
        return leftLimit > 0 ? leftLimit : intendingToPopoverLeft;
    }

    function calculatePopoverPostionOnTop(popoverRect: DOMRect, referenceRect: DOMRect, arrowRect: DOMRect): PopoverPosition {
        const arrowLeft = arrowLeftPosition.value;
        const popoverLeft = (referenceRect.left - hostLeft.value) + referenceRect.width / 2 + scrollLeft.value;
        const popoverTop = (referenceRect.top - hostTop.value) - (popoverRect.height + arrowRect.height) + scrollTop.value;
        return { popoverLeft, popoverTop, arrowLeft };
    }

    function calculateTopPositionWhilePopupOnBottom(popoverRect: DOMRect, referenceRect: DOMRect, arrowRect: DOMRect) {
        return (referenceRect.top - hostTop.value) + (referenceRect.height + arrowRect.height) + scrollTop.value;
    }

    function calculateLeftPositionWhilePopupOnBottom(intendingToPopoverLeft: number, popoverRect: DOMRect) {
        const popoverLeftLimit = calculatePopoverLeftLimit(popoverRect, intendingToPopoverLeft);
        const arrowLeft = intendingToPopoverLeft <= popoverLeftLimit ? arrowLeftPosition.value : intendingToPopoverLeft - popoverLeftLimit;
        const popoverLeft = Math.min(intendingToPopoverLeft, popoverLeftLimit);
        return { popoverLeft, arrowLeft };
    }

    function calculatePopoverPositionOnBottom(popoverRect: DOMRect, referenceRect: DOMRect, arrowRect: DOMRect): PopoverPosition {
        const intendingToPopoverLeft = ((referenceRect.left - hostLeft.value) + referenceRect.width / 2) -
            (arrowRect.width / 2 - (popoverRect.left - hostLeft.value)) + scrollLeft.value;
        const { arrowLeft, popoverLeft } = calculateLeftPositionWhilePopupOnBottom(intendingToPopoverLeft, popoverRect);
        const popoverTop = calculateTopPositionWhilePopupOnBottom(popoverRect, referenceRect, arrowRect);
        return { popoverLeft, popoverTop, arrowLeft };
    }

    function calcultePopoverPositionOnBottomLeft(popoverRect: DOMRect, referenceRect: DOMRect, arrowRect: DOMRect): PopoverPosition {
        const intendingToPopoverLeft = (referenceRect.left - hostLeft.value) + scrollLeft.value;
        const { arrowLeft, popoverLeft } = calculateLeftPositionWhilePopupOnBottom(intendingToPopoverLeft, popoverRect);
        const popoverTop = calculateTopPositionWhilePopupOnBottom(popoverRect, referenceRect, arrowRect);
        return { popoverLeft, popoverTop, arrowLeft };
    }

    const postionMap = new Map<string, (popoverRect: DOMRect, referenceRect: DOMRect, arrowRect: DOMRect) => PopoverPosition>([
        ['top', calculatePopoverPostionOnTop],
        ['bottom', calculatePopoverPositionOnBottom],
        ['bottom-left', calcultePopoverPositionOnBottomLeft]
    ]);

    function locateToReference(referenceElement: HTMLElement) {
        const referenceRect = referenceElement.getBoundingClientRect();
        originalReferenceTop.value = referenceRect.top;
        originalReferenceLeft.value = referenceRect.left;
        const arrowRect = arrowRef.value.getBoundingClientRect();
        const popoverRect = popoverRef.value?.getBoundingClientRect();
        const calculatePopoverPosition = postionMap.get(position.value);
        if (calculatePopoverPosition) {
            const { arrowLeft, popoverLeft, popoverTop } = calculatePopoverPosition(popoverRect, referenceRect, arrowRect);
            arrowLeftPosition.value = arrowLeft;
            popoverLeftPosition.value = popoverLeft;
            popoverTopPosition.value = popoverTop;
        }
        if (props.keepWidthWithReference) {
            fitToReference(referenceElement);
        }
    }

    function followToReferencePosition(referenceElement: HTMLElement) {
        if (referenceElement) {
            const referenceRect = referenceElement.getBoundingClientRect() as DOMRect;
            if (referenceRect.left !== originalReferenceLeft.value || referenceRect.top !== originalReferenceTop.value) {
                locateToReference(referenceElement);
            }
        }
    }

    return {
        arrowStyle,
        popoverStyle,
        position,
        popoverWidth,
        fitToReference,
        followToReferencePosition,
        locateToReference
    };
}
